/*
 * Copyright (C) 2012 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.testing;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.testing.NullPointerTester.isNullable;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.reflect.AbstractInvocationHandler;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Set;

/**
 * Generates a dummy interface proxy that simply returns a dummy value for each method.
 *
 * @author Ben Yu
 */
@GwtIncompatible
abstract class DummyProxy
{

    /**
     * Returns a new proxy for {@code interfaceType}. Proxies of the same interface are equal to each
     * other if the {@link DummyProxy} instance that created the proxies are equal.
     */
    final <T> T newProxy(TypeToken<T> interfaceType)
    {
        Set<Class<?>> interfaceClasses = Sets.newLinkedHashSet();
        interfaceClasses.addAll(interfaceType.getTypes().interfaces().rawTypes());
        // Make the proxy serializable to work with SerializableTester
        interfaceClasses.add(Serializable.class);
        Object dummy =
                Proxy.newProxyInstance(
                        interfaceClasses.iterator().next().getClassLoader(),
                        interfaceClasses.toArray(new Class<?>[interfaceClasses.size()]),
                        new DummyHandler(interfaceType));
        @SuppressWarnings("unchecked") // interfaceType is T
        T result = (T) dummy;
        return result;
    }

    /**
     * Returns the dummy return value for {@code returnType}.
     */
    abstract <R> R dummyReturnValue(TypeToken<R> returnType);

    private class DummyHandler extends AbstractInvocationHandler implements Serializable
    {
        private final TypeToken<?> interfaceType;

        DummyHandler(TypeToken<?> interfaceType)
        {
            this.interfaceType = interfaceType;
        }

        @Override
        protected Object handleInvocation(Object proxy, Method method, Object[] args)
        {
            Invokable<?, ?> invokable = interfaceType.method(method);
            ImmutableList<Parameter> params = invokable.getParameters();
            for (int i = 0; i < args.length; i++)
            {
                Parameter param = params.get(i);
                if (!isNullable(param))
                {
                    checkNotNull(args[i]);
                }
            }
            return dummyReturnValue(interfaceType.resolveType(method.getGenericReturnType()));
        }

        @Override
        public int hashCode()
        {
            return identity().hashCode();
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof DummyHandler)
            {
                DummyHandler that = (DummyHandler) obj;
                return identity().equals(that.identity());
            }
            else
            {
                return false;
            }
        }

        private DummyProxy identity()
        {
            return DummyProxy.this;
        }

        @Override
        public String toString()
        {
            return "Dummy proxy for " + interfaceType;
        }

        // Since type variables aren't serializable, reduce the type down to raw type before
        // serialization.
        private Object writeReplace()
        {
            return new DummyHandler(TypeToken.of(interfaceType.getRawType()));
        }
    }
}
